﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.Mime
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Reflection;
    using System.Text;
    using System.Xml;

    using Mews.IO;
    using Mews.Text;
    using Mews.UnitTesting;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    /// <summary>Tests the <see cref="Message"/> methods.</summary>
    [TestClass]
    public sealed class MessageTest : TestBase
    {
        [SuppressMessage("Microsoft.Performance", "CA1802:UseLiteralsWhereAppropriate", Justification = "There's a conditional, CA bug?")]
        private static readonly int MessageLoopCount =
            1600 / (Environment.OSVersion.Platform == PlatformID.WinCE ? 40 : 1);

        private static readonly Assembly thisAssembly = typeof(MessageTest).Assembly;
        private static readonly string thisNamespace = typeof(MessageTest).Namespace;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Parses Python and our own test messages, converts the result to XML, compares the XML to the
        /// expected XML and collects rough performance data.</summary>
        [TestMethod()]
        public void MainTest()
        {
            double averageMilliseconds = 0.0;
            int messageCount = 0;

            for (int i = 1; i < 45; ++i)
            {
                using (var stream = GetPythonTestMessageStream(i))
                {
                    // 38 & 43 are rather pathological cases, which are not worth supporting at this point
                    averageMilliseconds += ParseAndCheckMessage(
                        stream, StringHelper.Format("PythonMsg{0:00}.xml", i), (i != 38) && (i != 43));
                    ++messageCount;
                }
            }

            foreach (var messageName in thisAssembly.GetManifestResourceNames())
            {
                string prefix = thisNamespace + ".TestMessages.";
                const string Extension = ".eml";

                if (messageName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) &&
                    messageName.EndsWith(Extension, StringComparison.OrdinalIgnoreCase))
                {
                    string expectedXmlFilename = messageName.Substring(
                        prefix.Length, messageName.Length - prefix.Length - Extension.Length) + ".xml";

                    using (var seekableStream = StreamHelper.ToMemoryStream(
                        new FixLineEndingsStream(thisAssembly.GetManifestResourceStream(messageName)), 1024, 1024))
                    {
                        // We don't add the parsing time of our own messages, as these are not typical messages
                        ParseAndCheckMessage(seekableStream, 1, expectedXmlFilename, true);
                    }
                }
            }

            Console.WriteLine("Average time to parse one message: {0}ms", (double)averageMilliseconds / messageCount);
        }

        /// <summary>Tests whether the properties that give direct access to header fields return the correct data.
        /// </summary>
        [TestMethod]
        public void HeaderTest()
        {
            var displayName = GetUniqueString();
            var emailAddress = GetUniqueString();

            var normalAddress = new Address(displayName, emailAddress);
            Assert.AreEqual(displayName, normalAddress.DisplayName);
            Assert.AreEqual(emailAddress, normalAddress.EmailAddress);
            Assert.AreEqual(normalAddress.GetHashCode(), normalAddress.GetHashCode());

            var displayNameOnlyAddress = new Address(displayName, null);
            var emailAddressOnlyAddress = new Address(null, emailAddress);
            Assert.IsFalse(displayNameOnlyAddress.Equals(emailAddressOnlyAddress));
            Assert.IsFalse(displayNameOnlyAddress.Equals(null));

            normalAddress.GetHashCode();
            displayNameOnlyAddress.GetHashCode();
            emailAddressOnlyAddress.GetHashCode();

            var date = GetNowWithoutMilliseconds();
            var from = this.GetRandomAddresses();
            var sender = this.GetRandomAddresses();
            var replyTo = this.GetRandomAddresses();
            var to = this.GetRandomAddresses();
            var cc = this.GetRandomAddresses();
            var bcc = this.GetRandomAddresses();
            var messageId = GetUniqueString();
            var inReplyTo = GetUniqueString();
            var references = GetUniqueString();
            var subject = GetUniqueString();
            var comments = GetUniqueString();
            var keywords = GetUniqueString();
            var body = GetUniqueString() + "\r\n";

            var message = new Message()
            {
                Date = date,
                From = from,
                Sender = sender,
                ReplyTo = replyTo,
                To = to,
                Cc = cc,
                Bcc = bcc,
                MessageId = messageId,
                InReplyTo = new List<string>() { inReplyTo },
                References = new List<string>() { references },
                Subject = subject,
                Comments = comments,
                Keywords = keywords,
                Body = new TextBody(TextSubtypeNames.Plain) { Content = body }
            };

            using (var stream = new MemoryStream())
            {
                message.WriteTo(stream);
                stream.Seek(0, SeekOrigin.Begin);
                message = Message.Parse(stream);

                Assert.AreEqual(date, message.Date);
                CollectionAssert.AreEqual(from, (ICollection)message.From);
                CollectionAssert.AreEqual(sender, (ICollection)message.Sender);
                CollectionAssert.AreEqual(replyTo, (ICollection)message.ReplyTo);
                CollectionAssert.AreEqual(to, (ICollection)message.To);
                CollectionAssert.AreEqual(cc, (ICollection)message.Cc);
                CollectionAssert.AreEqual(bcc, (ICollection)message.Bcc);
                Assert.AreEqual(messageId, message.MessageId);
                Assert.AreEqual(inReplyTo, message.InReplyTo.First());
                Assert.AreEqual(references, message.References.First());
                Assert.AreEqual(subject, message.Subject);
                Assert.AreEqual(comments, message.Comments);
                Assert.AreEqual(keywords, message.Keywords);

                TextBody textBody = message.Body as TextBody;
                Assert.IsNotNull(textBody);
                Assert.AreEqual(body, textBody.Content);

                message.From = null;
                Assert.AreEqual(null, message.From);
            }

            using (var stream = new MemoryStream())
            {
                message = Message.Parse(stream);
                Assert.AreEqual(null, message.Date);
                Assert.AreEqual(null, message.From);
                Assert.AreEqual(null, message.Sender);
                Assert.AreEqual(null, message.ReplyTo);
                Assert.AreEqual(null, message.To);
                Assert.AreEqual(null, message.Cc);
                Assert.AreEqual(null, message.Bcc);
                Assert.AreEqual(null, message.MessageId);
                Assert.AreEqual(null, message.InReplyTo);
                Assert.AreEqual(null, message.References);
                Assert.AreEqual(null, message.Subject);
                Assert.AreEqual(null, message.Comments);
                Assert.AreEqual(null, message.Keywords);
                Assert.IsNull(message.Body);
            }
        }

        /// <summary>Tests <see cref="HeaderFieldCollection"/>.</summary>
        [TestMethod]
        public void HeaderFieldCollectionTest()
        {
            var field = new UnstructuredHeaderField(HeaderFieldNames.Subject);
            field.Body = GetUniqueString();
            var message = new Message();
            var headerFields = message.Fields;

            Assert.AreEqual("1.0", ((UnstructuredHeaderField)headerFields["MIME-Version"].FirstOrDefault()).Body);
            Assert.AreEqual(1, headerFields.Count);
            headerFields.Clear();
            Assert.AreEqual(0, headerFields.Count);

            Assert.IsFalse(headerFields.Remove(field));
            Assert.IsFalse(headerFields.Contains(field.Name));
            Assert.AreEqual(0, headerFields.Count);

            headerFields.Add(field);
            Assert.IsTrue(headerFields.Contains(field.Name));
            Assert.AreEqual(1, headerFields.Count);
            Assert.AreEqual(field.Body, message.Subject);
            AssertCount(headerFields, 1);

            Assert.IsTrue(headerFields.Remove(field));
            Assert.IsFalse(headerFields.Remove(field));
            Assert.IsFalse(headerFields.Contains(field.Name));
            Assert.AreEqual(0, headerFields.Count);
            Assert.AreEqual(null, message.Subject);
            AssertCount(headerFields, 0);

            headerFields.Add(field);
            headerFields.Add(field);
            Assert.AreEqual(2, headerFields.Count);
            Assert.AreEqual(2, headerFields.Remove(field.Name));
            Assert.AreEqual(0, headerFields.Count);
            Assert.AreEqual(0, headerFields.Remove(field.Name));

            // Check that incorrectly typed fields are not returned
            headerFields.Add(new DateHeaderField(HeaderFieldNames.Subject));
            Assert.AreEqual(null, message.Subject);

            AssertThrow<ArgumentNullException>(
                () => headerFields.Add(null),
                () => headerFields.Remove((HeaderField)null),
                () => headerFields.Remove((string)null));
        }

        /// <summary>Tests <see cref="HeaderField{T}"/> subclass constructors.</summary>
        [TestMethod]
        public void HeaderFieldConstructorTest()
        {
            AssertThrow<ArgumentNullException>(
                () => new ParameterizedHeaderField(HeaderFieldNames.ContentType).Value = null,
                () => new UnstructuredHeaderField(HeaderFieldNames.From).Body = null);
            AssertThrow<ArgumentException>(
                () => new DateHeaderField(null).ToString(),
                () => new DateHeaderField(string.Empty).ToString());
        }

        /// <summary>Tests <see cref="Body{T}"/> methods and subclass methods.</summary>
        [TestMethod]
        public void BodyTest()
        {
            AssertThrow<ArgumentException>(() => new ApplicationBody((string)null).ToString());
            AssertThrow<ArgumentException>(() => new ApplicationBody(string.Empty).ToString());
            AssertThrow<ArgumentNullException>(
                () => new ApplicationBody(ApplicationSubtypeNames.OctetStream).SetContent(null));

            new ImageBody(ImageSubtypeNames.Jpeg).GetContent().Dispose();

            AssertThrow<ArgumentNullException>(() => new MessageBody(MessageSubtypeNames.Rfc822).Content = null);

            var multipartBody = new MultipartBody(MultipartSubtypeNames.Mixed);
            AssertThrow<ArgumentNullException>(() => multipartBody.Preamble = null);
            AssertThrow<ArgumentNullException>(() => multipartBody.Epilogue = null);

            AssertThrow<ArgumentNullException>(() => new TextBody(TextSubtypeNames.Plain).Content = null);
        }

        /// <summary>Tests whether <see cref="Message.Parse(Stream)"/> throws the right exceptions.</summary>
        [TestMethod]
        public void ExceptionTest()
        {
            string body = "On the futility of life";
            string fieldName = "Subject";
            string input = fieldName + ": " + body + "\r\n";
            Assert.AreEqual(
                body,
                GetFieldBody(ExceptionTestParseMethod<Message>(Message.Parse, Message.Parse, input), fieldName));
            AssertThrow<ArgumentNullException>(() => new Message().WriteTo(null));
        }

        /// <summary>Tests <see cref="AddressListHeaderField"/> use cases.</summary>
        [TestMethod]
        public void AddressListHeaderFieldTest()
        {
            AssertThrow<ArgumentException>(
                () => new Address(null).ToString(),
                () => new Address(string.Empty).ToString(),
                () => new Address(null, null).ToString(),
                () => new Address(null, string.Empty).ToString(),
                () => new Address(string.Empty, null).ToString(),
                () => new Address(string.Empty, string.Empty).ToString());
            var displayName = GetUniqueString();
            var emailAddress = GetUniqueString();
            Assert.AreEqual(displayName, new Address(displayName).ToString());
            Assert.AreEqual(displayName, new Address(displayName, null).ToString());
            Assert.AreEqual(displayName, new Address(displayName, emailAddress).ToString());
            Assert.AreEqual(emailAddress, new Address(null, emailAddress).ToString());
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private static Stream GetPythonTestMessageStream(int messageNumber)
        {
            const string RemoteFilePattern = "msg_{0:00}.txt";
            const string LocalFilePattern = "PythonMsg{0:00}.eml";
            string remoteFilename = string.Format(CultureInfo.InvariantCulture, RemoteFilePattern, messageNumber);
            string localFilename = string.Format(CultureInfo.InvariantCulture, LocalFilePattern, messageNumber);

            string path = Path.Combine(
                Path.Combine(
                    Path.Combine(
                        Path.GetDirectoryName(typeof(MessageTest).Assembly.ManifestModule.FullyQualifiedName), ".."),
                    ".."),
                localFilename);

            if (!File.Exists(path))
            {
                string url = "http://svn.python.org/view/*checkout*/python/trunk/Lib/email/test/data/" + remoteFilename;

                // Surprisingly, the Python tests have LF-only line endings
                using (var response = WebRequest.Create(url).GetResponse())
                using (var webStream = new FixLineEndingsStream(response.GetResponseStream()))
                using (var fileStream = File.Create(path))
                {
                    StreamHelper.Copy(webStream, fileStream, new byte[8192]);
                }
            }

            using (var stream = File.OpenRead(path))
            {
                return StreamHelper.ToMemoryStream(stream, 8192, 1024);
            }
        }

        private static double ParseAndCheckMessage(Stream stream, string exptectedXmlFilename, bool testWrite)
        {
            ParseAndCheckMessage(stream, 1, exptectedXmlFilename, testWrite);
            return (double)ParseAndCheckMessage(stream, MessageLoopCount, exptectedXmlFilename, testWrite) / MessageLoopCount;
        }

        /// <summary>Parses the message contained in <paramref name="stream"/> <paramref name="loopCount"/> times and
        /// asserts that the XML representation is equal to the one specified with
        /// <paramref name="expectedXmlFilename"/>.</summary>
        /// <remarks>This function does two things at once. Ideally, these should be separated, but we'd then
        /// risk that future changes to the perf test code might use the parser in a wrong way and therefore silently
        /// measure the wrong thing.</remarks>
        private static int ParseAndCheckMessage(
            Stream stream, int loopCount, string expectedXmlFilename, bool testWrite)
        {
            int start = Environment.TickCount;
            Message message = null;

            for (int count = 0; count < loopCount; ++count)
            {
                stream.Seek(0, SeekOrigin.Begin);
                message = Message.Parse(stream);
            }

            int duration = Environment.TickCount - start;

            // If the following assert fails then the value of the MessageLoopCount constant should be increased
            Assert.IsTrue((loopCount == 1) || (duration != 0));
            AssertEqual(expectedXmlFilename, message);

            if (testWrite)
            {
                AssertEqualToWrittenMessage(message);
            }

            return duration;
        }

        private static void AssertEqual(string expectedXmlFilename, Message message)
        {
            using (var actualXmlStream = ToXmlStream(message, false))
            using (var expectedXmlStream = GetExpectedMessageContent(expectedXmlFilename))
            {
                AssertEqual(expectedXmlStream, actualXmlStream);
            }
        }

        private static void AssertEqualToWrittenMessage(Message message)
        {
            Message message2;

            using (var stream = new MemoryStream(1024))
            {
                message.WriteTo(stream);
                stream.Seek(0, SeekOrigin.Begin);
                message2 = Message.Parse(stream);
            }

            AssertEqual(ToXmlStream(message, true), ToXmlStream(message2, true));
        }

        private static Stream ToXmlStream(Message message, bool skipMimeFieldsAndFallbackReasons)
        {
            using (var streamGuard = ScopeGuard.Create(new MemoryStream(1024)))
            {
                using (var visitor = new ConvertToXmlVisitor(streamGuard.Resource, skipMimeFieldsAndFallbackReasons))
                {
                    message.Accept(visitor);
                }

                streamGuard.Resource.Seek(0, SeekOrigin.Begin);
                streamGuard.Dismiss();
                return streamGuard.Resource;
            }
        }

        private static Stream GetExpectedMessageContent(string expectedXmlFilename)
        {
            var expectedXmlPath = thisNamespace + ".ExpectedMessageContent." + expectedXmlFilename;
            var expectedXmlStream = thisAssembly.GetManifestResourceStream(expectedXmlPath);

            if (expectedXmlStream == null)
            {
                throw new ArgumentException(expectedXmlPath + " not found.");
            }

            return expectedXmlStream;
        }

        private static void AssertEqual(Stream expectedXmlStream, Stream actualXmlStream)
        {
            // We're deliberately ignoring comments in the expected XML, but not in the actual XML
            using (var expectedXmlReader = XmlReader.Create(
                expectedXmlStream,
                new XmlReaderSettings() { IgnoreComments = true, IgnoreWhitespace = true }))
            using (var actualXmlReader =
                XmlReader.Create(actualXmlStream, new XmlReaderSettings() { IgnoreWhitespace = true }))
            {
                try
                {
                    while (expectedXmlReader.Read())
                    {
                        Assert.IsTrue(actualXmlReader.Read());
                        AssertEqualState(expectedXmlReader, actualXmlReader);

                        if (expectedXmlReader.MoveToFirstAttribute())
                        {
                            Assert.IsTrue(actualXmlReader.MoveToFirstAttribute());
                            AssertEqualState(expectedXmlReader, actualXmlReader);

                            while (expectedXmlReader.MoveToNextAttribute())
                            {
                                Assert.IsTrue(actualXmlReader.MoveToNextAttribute());
                                AssertEqualState(expectedXmlReader, actualXmlReader);
                            }

                            Assert.IsFalse(actualXmlReader.MoveToNextAttribute());
                        }
                        else
                        {
                            Assert.IsFalse(actualXmlReader.MoveToFirstAttribute());
                        }
                    }

                    Assert.IsFalse(actualXmlReader.Read());
                }
                catch (AssertFailedException)
                {
                    Console.WriteLine("Expected:");
                    expectedXmlStream.Seek(0, SeekOrigin.Begin);
                    WriteToConsole(expectedXmlStream);

                    Console.WriteLine();
                    Console.WriteLine("Actual:");
                    actualXmlStream.Seek(0, SeekOrigin.Begin);
                    WriteToConsole(actualXmlStream);
                    throw;
                }
            }
        }

        private static void WriteToConsole(Stream stream)
        {
            using (var expectedReader = new StreamReader(stream, Encoding.UTF8))
            {
                Console.WriteLine(expectedReader.ReadToEnd());
            }
        }

        private static void AssertEqualState(XmlReader expectedXmlReader, XmlReader actualXmlReader)
        {
            Assert.AreEqual(expectedXmlReader.NodeType, actualXmlReader.NodeType);
            Assert.AreEqual(expectedXmlReader.Name, actualXmlReader.Name);
            Assert.AreEqual(expectedXmlReader.Value, actualXmlReader.Value);
        }

        private static string GetFieldBody(Entity entity, string fieldName)
        {
            return ((UnstructuredHeaderField)entity.Fields[fieldName].FirstOrDefault()).Body;
        }

        private static TResult ExceptionTestParseMethod<TResult>(
            Func<Stream, TResult> parseMethod1, Func<Stream, int, TResult> parseMethod2, string input)
        {
            AssertThrow<ArgumentNullException>(() => parseMethod1(null));
            AssertThrow<ArgumentOutOfRangeException>(() => Parse<TResult>(input2 => parseMethod2(input2, -1), input));
            TResult result = Parse<TResult>(parseMethod1, input);
            AssertThrow<ArgumentException>(
                () => Parse<TResult>(input2 => parseMethod2(input2, input.Length - 1), input));
            return result;
        }

        private static TResult Parse<TResult>(Func<Stream, TResult> parseMethod, string input)
        {
            return parseMethod(new MemoryStream(Encoding.ASCII.GetBytes(input)));
        }

        private static DateTime GetNowWithoutMilliseconds()
        {
            DateTime now = DateTime.UtcNow;
            return new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, now.Kind);
        }

        private static void AssertCount(HeaderFieldCollection headerFields, int expectedCount)
        {
            int actualCount = 0;

            foreach (IGrouping<string, HeaderField> grouping in (IEnumerable)headerFields)
            {
                Assert.IsFalse(string.IsNullOrEmpty(grouping.Key));

                foreach (var body in (IEnumerable)grouping)
                {
                    ++actualCount;
                }
            }

            Assert.AreEqual(expectedCount, actualCount);
        }

        private Address[] GetRandomAddresses()
        {
            var addresses = new Address[Random.Next(10)];

            for (int index = 0; index < addresses.Length; ++index)
            {
                addresses[index] = new Address(GetUniqueString(), GetUniqueString());
            }

            return addresses;
        }

        /// <summary>Writes the structure of a message into the <see cref="StreamWriter"/> passed to the constructor.
        /// </summary>
        private sealed class ConvertToXmlVisitor : MessageVisitor, IHeaderFieldVisitor, IDisposable
        {
            private readonly bool skipMimeFieldsAndFallbackReasons;
            private readonly XmlWriter writer;

            /// <summary>See <see cref="MessageVisitor.OnVisit(ApplicationBody)"/>.</summary>
            public sealed override void OnVisit(ApplicationBody visitable)
            {
                this.WriteStartElement(visitable);
                byte[] buffer = visitable.GetContent();
                this.writer.WriteBase64(buffer, 0, buffer.Length);
                this.WriteEndElement();
            }

            /// <summary>See <see cref="MessageVisitor.OnVisit(ImageBody)"/>.</summary>
            public sealed override void OnVisit(ImageBody visitable)
            {
                this.WriteStartElement(visitable);
                byte[] buffer = visitable.GetContentBytes();
                this.writer.WriteBase64(buffer, 0, buffer.Length);
                this.WriteEndElement();
            }

            /// <summary>See <see cref="MessageVisitor.OnVisit(TextBody)"/>.</summary>
            public sealed override void OnVisit(TextBody visitable)
            {
                this.WriteStartElement(visitable);
                this.writer.WriteAttributeString("format", visitable.Format.ToString());
                this.WriteString(visitable.Content);
                this.WriteEndElement();
            }

            /// <summary>See <see cref="MessageVisitor.OnVisit(MessageBody)"/>.</summary>
            public sealed override void OnVisit(MessageBody visitable)
            {
                this.WriteStartElement(visitable);
                base.OnVisit(visitable);
                this.WriteEndElement();
            }

            /// <summary>See <see cref="MessageVisitor.OnVisit(MultipartBody)"/>.</summary>
            public sealed override void OnVisit(MultipartBody visitable)
            {
                this.WriteStartElement(visitable);
                this.writer.WriteElementString("Preamble", visitable.Preamble);
                base.OnVisit(visitable);
                this.writer.WriteElementString("Epilogue", visitable.Epilogue);
                this.WriteEndElement();
            }

            /// <summary>See <see cref="MessageVisitor.OnVisit(BodyPart)"/>.</summary>
            public sealed override void OnVisit(BodyPart visitable)
            {
                this.WriteStartElement(visitable);
                this.OnVisitEntity(visitable);
                base.OnVisit(visitable);
                this.WriteEndElement();
            }

            /// <summary>See <see cref="MessageVisitor.OnVisit(Message)"/>.</summary>
            public sealed override void OnVisit(Message visitable)
            {
                this.WriteStartElement(visitable);
                this.OnVisitEntity(visitable);
                base.OnVisit(visitable);
                this.WriteEndElement();
            }

            /// <summary>See <see cref="IVisitor{T}.OnVisit"/>.</summary>
            public void OnVisit(HeaderField visitable)
            {
                throw GetNotImplementedException(visitable);
            }

            /// <summary>See <see cref="IVisitor{T}.OnVisit"/>.</summary>
            public void OnVisit(DateHeaderField visitable)
            {
                this.WriteStartElement(visitable);
                this.OnVisitHeaderField(visitable);
                this.WriteString(visitable.DateUtc.ToString("U", CultureInfo.InvariantCulture));
                this.WriteEndElement();
            }

            /// <summary>See <see cref="IVisitor{T}.OnVisit"/>.</summary>
            public void OnVisit(AddressListHeaderField visitable)
            {
                this.WriteStartElement(visitable);
                this.OnVisitHeaderField(visitable);

                foreach (var address in visitable.Addresses)
                {
                    this.writer.WriteStartElement("Address");
                    this.writer.WriteAttributeString("displayName", address.DisplayName);
                    this.writer.WriteString(address.EmailAddress);
                    this.writer.WriteEndElement();
                }

                this.WriteEndElement();
            }

            /// <summary>See <see cref="IVisitor{T}.OnVisit"/>.</summary>
            public void OnVisit(ParameterizedHeaderField visitable)
            {
                if (this.skipMimeFieldsAndFallbackReasons &&
                    (visitable.Name.Equals(HeaderFieldNames.ContentType, StringComparison.OrdinalIgnoreCase) ||
                    visitable.Name.Equals(HeaderFieldNames.ContentTransferEncoding, StringComparison.OrdinalIgnoreCase) ||
                    visitable.Name.Equals(HeaderFieldNames.ContentDisposition, StringComparison.OrdinalIgnoreCase)))
                {
                    return;
                }

                this.WriteStartElement(visitable);
                this.OnVisitHeaderField(visitable);
                this.writer.WriteAttributeString("value", visitable.Value);

                foreach (var entry in visitable.Parameters)
                {
                    this.writer.WriteStartElement("Parameter");
                    this.writer.WriteAttributeString("name", entry.Key);
                    this.WriteString(entry.Value);
                    this.writer.WriteEndElement();
                }

                this.WriteEndElement();
            }

            /// <summary>See <see cref="IVisitor{T}.OnVisit"/>.</summary>
            public void OnVisit(ReferencesHeaderField visitable)
            {
                this.WriteStartElement(visitable);
                this.OnVisitHeaderField(visitable);

                foreach (var entry in visitable.References)
                {
                    this.writer.WriteStartElement("Reference");
                    this.WriteString(entry);
                    this.writer.WriteEndElement();
                }

                this.WriteEndElement();
            }

            /// <summary>See <see cref="IVisitor{T}.OnVisit"/>.</summary>
            public void OnVisit(UnstructuredHeaderField visitable)
            {
                this.WriteStartElement(visitable);
                this.OnVisitHeaderField(visitable);
                this.WriteString(visitable.Body);
                this.WriteEndElement();
            }

            /// <summary>See <see cref="IDisposable.Dispose"/>.</summary>
            public void Dispose()
            {
                ((IDisposable)this.writer).Dispose();
            }

            ////////////////////////////////////////////////////////////////////////////////////////////////////////////

            internal ConvertToXmlVisitor(Stream stream, bool skipMimeFieldsAndFallbackReasons)
            {
                this.skipMimeFieldsAndFallbackReasons = skipMimeFieldsAndFallbackReasons;
                this.writer = XmlWriter.Create(stream, new XmlWriterSettings() { Indent = true, IndentChars = "    " });
            }

            ////////////////////////////////////////////////////////////////////////////////////////////////////////////

            private void WriteStartElement(Body visitable)
            {
                this.WriteStartElement((object)visitable);
                this.writer.WriteAttributeString("subtype", visitable.Subtype);

                if (visitable.Disposition != null)
                {
                    this.writer.WriteAttributeString("dispositionKind", visitable.Disposition.Kind.ToString());

                    if (visitable.Disposition.FileName != null)
                    {
                        this.writer.WriteAttributeString("dispositionFileName", visitable.Disposition.FileName);
                    }
                }

                if (!this.skipMimeFieldsAndFallbackReasons && (visitable.FallbackReason != null))
                {
                    this.writer.WriteAttributeString("fallbackReasonKind", visitable.FallbackReason.Kind.ToString());
                    this.writer.WriteAttributeString("fallbackReasonData", visitable.FallbackReason.Data);
                }
            }

            private void WriteStartElement(object visitable)
            {
                this.writer.WriteStartElement(visitable.GetType().Name);
            }

            private void WriteEndElement()
            {
                this.writer.WriteEndElement();
            }

            private void WriteString(string value)
            {
                if (!string.IsNullOrEmpty(value))
                {
                    this.writer.WriteString(value);
                }
            }

            private void OnVisitEntity(Entity visitable)
            {
                this.writer.WriteStartElement("Header");

                foreach (var grouping in visitable.Fields)
                {
                    foreach (var field in grouping)
                    {
                        field.Accept(this);
                    }
                }

                this.WriteEndElement();
            }

            private void OnVisitHeaderField(HeaderField visitable)
            {
                this.writer.WriteAttributeString("name", visitable.Name);
            }
        }
    }
}
